<script>
class Vue{
    constructor(options){
        this.$el = options.el
        this.$data = options.data
        new Observer(this.$data)
        new Compiler(this.$el, this)
    }
}
class Observer{
    constructor(data){
        this.observer(data)
    }
    observer(data){
        if(data && typeof data === 'object'){
            for(let key in data){
                this.defineReactive(data, key, data[key])
            }
        }
    }
    defineReactive(obj, key, value){
        this.observer(value)
        let dep = new Dep()
        Object.defineProperty(obj, key, {
            get(){
                Dep.target && dep.add(Dep.target)
                return value
            },
            set: newValue => {
                if(newValue !== value){
                    this.observer(newValue)
                    value = newValue
                    dep.notify()
                }
            }
        })
    }
}
//dependence 依赖
class Dep{
    constructor(){
        this.subs = []
    }
    addSub(watcher){
        this.subs.push(watcher)
    }
    notify(){
        this.subs.forEach(item => {
            item.update()
        })
    }
}
//订阅者
class Watcher{
    constructor(vm, expr, cb){
        this.vm = vm
        this.expr = expr
        this.cb = cb
        this.oldValue = this.get()
    }
    get(){
        Dep.target = this
        let value = CompileUtil.getVal(this.vm, this.expr)
        Dep.target = null
        return value
    }
    update(){
        let newValue = this.get()
        if(newValue !== this.oldValue){
            this.cb(newValue)
        }
    }
}

class Compiler{
    constructor(el, vm){
        this.el = el.nodeType===1 ? el : document.querySelector(el)
        this.vm = vm
        let fragment = this.node2fragment(this.el)
        this.compile(fragment)
        this.el.appendChild(fragment)
    }
    node2fragment(node){
        let fragment = document.createDocumentFragment()
        let firstChild
        while(firstChild = node.firstChild){
            fragment.appendChild(firstChild)
        }
        return fragment
    }
    compile(node){
        let childNodes = node.childNodes
        ;[...childNodes].forEach(child => {
            if(child.nodeType === 1){
                this.compileElement(child)
                this.compile(child)
            }else{
                this.compileText(child)
            }
        })
    }
    compileElement(node){
        let attributes = node.attributes
        ;[...attributes].forEach(attr => {
            let {name, value:expr} = attr
            if(this.isDirective(name)){
                let [, directive] = name.split('-')
                CompileUtil[directive](node, expr, this.vm)
            }
        })
    }
    compileText(node){
        let content = node.textContent
        if(/\{\{(.+?)\}\}/.test(content)){
            CompileUtil['text'](node, content, this.vm)
        }
    }
    isDirective(attrName){
        return attrName.startsWith('v-')
    }
}

CompileUtil = {
    model(node, expr, vm){
        let fn = this.updater['modelUpdater']
        new Watcher(vm, expr, newValue => {
            fn(node, newValue)
        })
        let value = this.getVal(expr, vm)
        fn(node, value)
    },
    html(){

    },
    text(node, content, vm){
        let fn = this.updater['textUpdater']
        let newContent = content.replace(/\{\{(.+?)\}\}/g, (...args) => {
            new Watcher(vm, args[1], () => {
                let nContent = content.replace(/\{\{(.+?)\}\}/g, (...args2) => {
                    return this.getVal(vm, args2[1])
                })
                fn(node, nContent)
            })
            return this.getVal(vm, args[1])
        })
        fn(node, newContent)
    },
    updater:{
        modelUpdater(node, value){
            node.value = value
        },
        htmlUpdater(){

        },
        textUpdater(node, content){
            node.content = content
        }
    },
    getVal(expr, vm){
        expr.split('.').reduce((data, current) => {
            return data[current]
        }, vm.$data)
    }
}


</script>